/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.vertx.core.http.impl;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.CaseInsensitiveHeaders;
import io.vertx.core.http.HttpServerRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import static io.vertx.core.http.Http2Settings.DEFAULT_ENABLE_PUSH;
import static io.vertx.core.http.Http2Settings.DEFAULT_HEADER_TABLE_SIZE;
import static io.vertx.core.http.Http2Settings.DEFAULT_INITIAL_WINDOW_SIZE;
import static io.vertx.core.http.Http2Settings.DEFAULT_MAX_CONCURRENT_STREAMS;
import static io.vertx.core.http.Http2Settings.DEFAULT_MAX_FRAME_SIZE;
import static io.vertx.core.http.Http2Settings.DEFAULT_MAX_HEADER_LIST_SIZE;
/**
* Various http utils.
*
* @author <a href="mailto:nmaurer@redhat.com">Norman Maurer</a>
*/
public final class HttpUtils {
private HttpUtils() {
}
private static int indexOfSlash(CharSequence str, int start) {
for (int i = start; i < str.length(); i++) {
if (str.charAt(i) == '/') {
return i;
}
}
return -1;
}
private static boolean matches(CharSequence path, int start, String what) {
return matches(path, start, what, false);
}
private static boolean matches(CharSequence path, int start, String what, boolean exact) {
if (exact) {
if (path.length() - start != what.length()) {
return false;
}
}
if (path.length() - start >= what.length()) {
for (int i = 0; i < what.length(); i++) {
if (path.charAt(start + i) != what.charAt(i)) {
return false;
}
}
return true;
}
return false;
}
/**
* Removed dots as per <a href="http://tools.ietf.org/html/rfc3986#section-5.2.4>rfc3986</a>.
*
* There are 2 extra transformations that are not part of the spec but kept for backwards compatibility:
*
* double slash // will be converted to single slash and the path will always start with slash.
*
* @param path raw path
* @return normalized path
*/
public static String removeDots(CharSequence path) {
if (path == null) {
return null;
}
final StringBuilder obuf = new StringBuilder(path.length());
int i = 0;
while (i < path.length()) {
// remove dots as described in
// http://tools.ietf.org/html/rfc3986#section-5.2.4
if (matches(path, i, "./")) {
i += 2;
} else if (matches(path, i, "../")) {
i += 3;
} else if (matches(path, i, "/./")) {
// preserve last slash
i += 2;
} else if (matches(path, i,"/.", true)) {
path = "/";
i = 0;
} else if (matches(path, i, "/../")) {
// preserve last slash
i += 3;
int pos = obuf.lastIndexOf("/");
if (pos != -1) {
obuf.delete(pos, obuf.length());
}
} else if (matches(path, i, "/..", true)) {
path = "/";
i = 0;
int pos = obuf.lastIndexOf("/");
if (pos != -1) {
obuf.delete(pos, obuf.length());
}
} else if (matches(path, i, ".", true) || matches(path, i, "..", true)) {
break;
} else {
if (path.charAt(i) == '/') {
i++;
// Not standard!!!
// but common // -> /
if (obuf.length() == 0 || obuf.charAt(obuf.length() - 1) != '/') {
obuf.append('/');
}
}
int pos = indexOfSlash(path, i);
if (pos != -1) {
obuf.append(path, i, pos);
i = pos;
} else {
obuf.append(path, i, path.length());
break;
}
}
}
return obuf.toString();
}
/**
* Resolve an URI reference as per <a href="http://tools.ietf.org/html/rfc3986#section-5.2.4>rfc3986</a>
*/
public static URI resolveURIReference(String base, String ref) throws URISyntaxException {
return resolveURIReference(URI.create(base), ref);
}
/**
* Resolve an URI reference as per <a href="http://tools.ietf.org/html/rfc3986#section-5.2.4>rfc3986</a>
*/
public static URI resolveURIReference(URI base, String ref) throws URISyntaxException {
URI _ref = URI.create(ref);
String scheme;
String authority;
String path;
String query;
if (_ref.getScheme() != null) {
scheme = _ref.getScheme();
authority = _ref.getAuthority();
path = removeDots(_ref.getPath());
query = _ref.getQuery();
} else {
if (_ref.getAuthority() != null) {
authority = _ref.getAuthority();
path = _ref.getPath();
query = _ref.getQuery();
} else {
if (_ref.getPath().length() == 0) {
path = base.getPath();
if (_ref.getQuery() != null) {
query = _ref.getQuery();
} else {
query = base.getQuery();
}
} else {
if (_ref.getPath().startsWith("/")) {
path = removeDots(_ref.getPath());
} else {
// Merge paths
String mergedPath;
String basePath = base.getPath();
if (base.getAuthority() != null && basePath.length() == 0) {
mergedPath = "/" + _ref.getPath();
} else {
int index = basePath.lastIndexOf('/');
if (index > -1) {
mergedPath = basePath.substring(0, index + 1) + _ref.getPath();
} else {
mergedPath = _ref.getPath();
}
}
path = removeDots(mergedPath);
}
query = _ref.getQuery();
}
authority = base.getAuthority();
}
scheme = base.getScheme();
}
return new URI(scheme, authority, path, query, _ref.getFragment());
}
/**
* Extract the path out of the uri.
*/
static String parsePath(String uri) {
int i;
if (uri.charAt(0) == '/') {
i = 0;
} else {
i = uri.indexOf("://");
if (i == -1) {
i = 0;
} else {
i = uri.indexOf('/', i + 3);
if (i == -1) {
// contains no /
return "/";
}
}
}
int queryStart = uri.indexOf('?', i);
if (queryStart == -1) {
queryStart = uri.length();
}
return uri.substring(i, queryStart);
}
/**
* Extract the query out of a uri or returns {@code null} if no query was found.
*/
static String parseQuery(String uri) {
int i = uri.indexOf('?');
if (i == -1) {
return null;
} else {
return uri.substring(i + 1 , uri.length());
}
}
static String absoluteURI(String serverOrigin, HttpServerRequest req) throws URISyntaxException {
String absoluteURI;
URI uri = new URI(req.uri());
String scheme = uri.getScheme();
if (scheme != null && (scheme.equals("http") || scheme.equals("https"))) {
absoluteURI = uri.toString();
} else {
String host = req.host();
if (host != null) {
absoluteURI = req.scheme() + "://" + host + uri;
} else {
// Fall back to the server origin
absoluteURI = serverOrigin + uri;
}
}
return absoluteURI;
}
static MultiMap params(String uri) {
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri);
Map<String, List<String>> prms = queryStringDecoder.parameters();
MultiMap params = new CaseInsensitiveHeaders();
if (!prms.isEmpty()) {
for (Map.Entry<String, List<String>> entry: prms.entrySet()) {
params.add(entry.getKey(), entry.getValue());
}
}
return params;
}
public static void fromVertxInitialSettings(boolean server, io.vertx.core.http.Http2Settings vertxSettings, Http2Settings nettySettings) {
if (vertxSettings != null) {
if (!server && vertxSettings.isPushEnabled() != DEFAULT_ENABLE_PUSH) {
nettySettings.pushEnabled(vertxSettings.isPushEnabled());
}
if (vertxSettings.getHeaderTableSize() != DEFAULT_HEADER_TABLE_SIZE) {
nettySettings.put('\u0001', (Long)vertxSettings.getHeaderTableSize());
}
if (vertxSettings.getInitialWindowSize() != DEFAULT_INITIAL_WINDOW_SIZE) {
nettySettings.initialWindowSize(vertxSettings.getInitialWindowSize());
}
if (vertxSettings.getMaxConcurrentStreams() != DEFAULT_MAX_CONCURRENT_STREAMS) {
nettySettings.maxConcurrentStreams(vertxSettings.getMaxConcurrentStreams());
}
if (vertxSettings.getMaxFrameSize() != DEFAULT_MAX_FRAME_SIZE) {
nettySettings.maxFrameSize(vertxSettings.getMaxFrameSize());
}
if (vertxSettings.getMaxHeaderListSize() != DEFAULT_MAX_HEADER_LIST_SIZE) {
nettySettings.maxHeaderListSize(vertxSettings.getMaxHeaderListSize());
}
Map<Integer, Long> extraSettings = vertxSettings.getExtraSettings();
if (extraSettings != null) {
extraSettings.forEach((code, setting) -> {
nettySettings.put((char)(int)code, setting);
});
}
}
}
public static Http2Settings fromVertxSettings(io.vertx.core.http.Http2Settings settings) {
Http2Settings converted = new Http2Settings();
converted.pushEnabled(settings.isPushEnabled());
converted.maxFrameSize(settings.getMaxFrameSize());
converted.initialWindowSize(settings.getInitialWindowSize());
converted.headerTableSize(settings.getHeaderTableSize());
converted.maxConcurrentStreams(settings.getMaxConcurrentStreams());
converted.maxHeaderListSize(settings.getMaxHeaderListSize());
if (settings.getExtraSettings() != null) {
settings.getExtraSettings().forEach((key, value) -> {
converted.put((char)(int)key, value);
});
}
return converted;
}
public static io.vertx.core.http.Http2Settings toVertxSettings(Http2Settings settings) {
io.vertx.core.http.Http2Settings converted = new io.vertx.core.http.Http2Settings();
Boolean pushEnabled = settings.pushEnabled();
if (pushEnabled != null) {
converted.setPushEnabled(pushEnabled);
}
Long maxConcurrentStreams = settings.maxConcurrentStreams();
if (maxConcurrentStreams != null) {
converted.setMaxConcurrentStreams(maxConcurrentStreams);
}
Long maxHeaderListSize = settings.maxHeaderListSize();
if (maxHeaderListSize != null) {
converted.setMaxHeaderListSize(maxHeaderListSize);
}
Integer maxFrameSize = settings.maxFrameSize();
if (maxFrameSize != null) {
converted.setMaxFrameSize(maxFrameSize);
}
Integer initialWindowSize = settings.initialWindowSize();
if (initialWindowSize != null) {
converted.setInitialWindowSize(initialWindowSize);
}
Long headerTableSize = settings.headerTableSize();
if (headerTableSize != null) {
converted.setHeaderTableSize(headerTableSize);
}
settings.forEach((key, value) -> {
if (key > 6) {
converted.set(key, value);
}
});
return converted;
}
static Http2Settings decodeSettings(String base64Settings) {
try {
Http2Settings settings = new Http2Settings();
Buffer buffer = Buffer.buffer(Base64.getUrlDecoder().decode(base64Settings));
int pos = 0;
int len = buffer.length();
while (pos < len) {
int i = buffer.getUnsignedShort(pos);
pos += 2;
long j = buffer.getUnsignedInt(pos);
pos += 4;
settings.put((char)i, (Long)j);
}
return settings;
} catch (Exception ignore) {
}
return null;
}
private static class CustomCompressor extends HttpContentCompressor {
@Override
public ZlibWrapper determineWrapper(String acceptEncoding) {
return super.determineWrapper(acceptEncoding);
}
}
private static final CustomCompressor compressor = new CustomCompressor();
static String determineContentEncoding(Http2Headers headers) {
String acceptEncoding = headers.get(HttpHeaderNames.ACCEPT_ENCODING) != null ? headers.get(HttpHeaderNames.ACCEPT_ENCODING).toString() : null;
if (acceptEncoding != null) {
ZlibWrapper wrapper = compressor.determineWrapper(acceptEncoding);
if (wrapper != null) {
switch (wrapper) {
case GZIP:
return "gzip";
case ZLIB:
return "deflate";
}
}
}
return null;
}
static HttpMethod toNettyHttpMethod(io.vertx.core.http.HttpMethod method, String rawMethod) {
switch (method) {
case CONNECT: {
return HttpMethod.CONNECT;
}
case GET: {
return HttpMethod.GET;
}
case PUT: {
return HttpMethod.PUT;
}
case POST: {
return HttpMethod.POST;
}
case DELETE: {
return HttpMethod.DELETE;
}
case HEAD: {
return HttpMethod.HEAD;
}
case OPTIONS: {
return HttpMethod.OPTIONS;
}
case TRACE: {
return HttpMethod.TRACE;
}
case PATCH: {
return HttpMethod.PATCH;
}
default: {
return HttpMethod.valueOf(rawMethod);
}
}
}
static HttpVersion toNettyHttpVersion(io.vertx.core.http.HttpVersion version) {
switch (version) {
case HTTP_1_0: {
return HttpVersion.HTTP_1_0;
}
case HTTP_1_1: {
return HttpVersion.HTTP_1_1;
}
default:
throw new IllegalArgumentException("Unsupported HTTP version: " + version);
}
}
static io.vertx.core.http.HttpMethod toVertxMethod(String method) {
try {
return io.vertx.core.http.HttpMethod.valueOf(method);
} catch (IllegalArgumentException e) {
return io.vertx.core.http.HttpMethod.OTHER;
}
}
}